封装上传组件逻辑:useUpload(包含预览、删除、隐藏操作区)
概述
课程封面上传组件中包含大量重复逻辑(文件列表管理、预览、删除、操作区显隐等),通过封装 useUpload 组合式函数和子组件,实现上传逻辑的复用与关注点分离。核心思路是将 JSX 语法中的重复代码拆分为独立的 Vue 单文件组件,配合组合式函数统一管理状态。
组件拆分架构
Upload 组件目录
├── components/
│ └── contents/
│ └── upload/
│ ├── UploadTrigger.vue # 上传触发区域(slot、拖拽提示)
│ ├── FilePreview.vue # 文件预览(图片、删除按钮、操作区)
│ └── useUpload.ts # 上传逻辑组合式函数
text
UploadTrigger 子组件
封装上传触发区域,包含默认 slot 内容和拖拽提示。从原先 JSX 语法中提取结构,改写为 Vue 模板语法:
<!-- components/contents/upload/UploadTrigger.vue -->
<template>
<el-upload
action="#"
:auto-upload="false"
:show-file-list="false"
:on-change="handleChange"
accept="image/*"
>
<slot>
<div class="upload-trigger">
<el-icon class="upload-icon"><Plus /></el-icon>
<div class="upload-tip">点击或拖拽上传</div>
</div>
</slot>
</el-upload>
</template>
<script setup lang="ts">
import type { UploadFile } from 'element-plus'
const emit = defineEmits<{
(e: 'change', file: UploadFile): void
}>()
const handleChange = (file: UploadFile) => {
emit('change', file)
}
</script>
vue
注意:从 JSX 迁移到模板语法时,事件绑定需要调整:
onClick->@clickonChange->@change或:on-changeonMouseenter->@mouseenter
FilePreview 子组件
封装文件预览区域,支持预览、删除和操作区显隐:
<!-- components/contents/upload/FilePreview.vue -->
<template>
<div
v-for="(file, index) in fileList"
:key="index"
class="file-preview"
@mouseenter="showActions(index)"
@mouseleave="hideActions(index)"
>
<img :src="file.url" class="preview-image" />
<div v-show="file.showActions" class="preview-actions">
<el-icon @click="handlePreview(file)"><ZoomIn /></el-icon>
<el-icon @click="handleDelete(index)"><Delete /></el-icon>
</div>
</div>
</template>
<script setup lang="ts">
interface FileItem {
url: string
name: string
showActions?: boolean
}
const props = defineProps<{
fileList: FileItem[]
}>()
const emit = defineEmits<{
(e: 'preview', file: FileItem): void
(e: 'delete', index: number): void
}>()
const showActions = (index: number) => {
if (props.fileList[index]) {
props.fileList[index].showActions = true
}
}
const hideActions = (index: number) => {
if (props.fileList[index]) {
props.fileList[index].showActions = false
}
}
const handlePreview = (file: FileItem) => {
emit('preview', file)
}
const handleDelete = (index: number) => {
emit('delete', index)
}
</script>
<style scoped>
.file-preview {
position: relative;
display: inline-block;
width: 148px;
height: 148px;
border-radius: 6px;
overflow: hidden;
}
.preview-image {
width: 100%;
height: 100%;
object-fit: cover;
}
.preview-actions {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
gap: 12px;
background-color: var(--el-overlay-color-lighter);
color: #fff;
font-size: 20px;
cursor: pointer;
}
</style>
vue
useUpload 组合式函数
// components/contents/upload/useUpload.ts
import { ref, reactive } from 'vue'
import type { UploadFile } from 'element-plus'
interface FileItem {
url: string
name: string
showActions: boolean
}
export function useUpload(options?: { maxCount?: number }) {
const fileList = reactive<FileItem[]>([])
const dialogVisible = ref(false)
const dialogImageUrl = ref('')
const maxCount = options?.maxCount ?? 5
const handleFileChange = (file: UploadFile) => {
if (fileList.length >= maxCount) return
fileList.push({
url: URL.createObjectURL(file.raw!),
name: file.name,
showActions: false,
})
}
const handlePreview = (file: FileItem) => {
dialogImageUrl.value = file.url
dialogVisible.value = true
}
const handleDelete = (index: number) => {
const removed = fileList.splice(index, 1)[0]
// 释放 blob URL,避免内存泄漏
if (removed?.url?.startsWith('blob:')) {
URL.revokeObjectURL(removed.url)
}
}
return {
fileList,
dialogVisible,
dialogImageUrl,
handleFileChange,
handlePreview,
handleDelete,
}
}
ts
父组件使用示例
拆分后,父组件代码大幅简化:
<template>
<div>
<UploadTrigger @change="handleFileChange" />
<FilePreview
:file-list="fileList"
@preview="handlePreview"
@delete="handleDelete"
/>
<el-dialog v-model="dialogVisible">
<img :src="dialogImageUrl" style="width: 100%" />
</el-dialog>
</div>
</template>
<script setup lang="ts">
import UploadTrigger from './upload/UploadTrigger.vue'
import FilePreview from './upload/FilePreview.vue'
import { useUpload } from './upload/useUpload'
const {
fileList,
dialogVisible,
dialogImageUrl,
handleFileChange,
handlePreview,
handleDelete,
} = useUpload({ maxCount: 1 })
</script>
vue
对比拆分前后的代码量:
- 拆分前:所有逻辑集中在父组件中,包含 JSX 语法、事件绑定、样式定义,约 200+ 行
- 拆分后:父组件仅约 20 行,子组件各约 50 行,逻辑清晰,可复用
组件职责划分
| 组件/函数 | 职责 | 关键 Props/Events |
|---|---|---|
UploadTrigger | 上传触发区域、文件选择 | @change |
FilePreview | 文件预览、操作区显隐 | fileList、@preview、@delete |
useUpload | 上传状态管理、文件操作 | fileList、handleFileChange 等 |
URL.createObjectURL 与内存管理
URL.createObjectURL 创建的 blob URL 会占用浏览器内存,需要手动释放:
// 创建:上传时生成预览 URL
const url = URL.createObjectURL(file.raw!)
// 释放:删除文件时回收内存
URL.revokeObjectURL(url)
ts
| 方法 | 说明 |
|---|---|
URL.createObjectURL(blob) | 为 Blob/File 对象创建临时 URL |
URL.revokeObjectURL(url) | 释放之前创建的 URL,回收内存 |
注意:如果不调用 revokeObjectURL,blob URL 会持续占用内存直到页面卸载(document unload)。在上传组件中,每次删除文件都应释放对应的 URL。
实践要点
- 组件拆分后,JSX 语法中的事件绑定需要改为模板语法(如
onClick->@click) - 预览图使用
URL.createObjectURL创建临时 URL,删除时需调用URL.revokeObjectURL释放内存 - 操作区的显隐通过
mouseenter/mouseleave事件控制showActions状态 - 子组件通过
emit通知父组件操作事件,由useUpload统一管理状态 useUpload的maxCount参数控制最大上传数量,默认 5
↑